using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Collections.Generic;
using System.Globalization;
using System.Diagnostics;
using System.IO;
using System.Xml;
using System.Xml.Schema;

namespace Dida.Dxml
{
	[AttributeUsage(AttributeTargets.Class|AttributeTargets.Struct|
		AttributeTargets.Enum,AllowMultiple=false)]
	public class ParsableAttribute : Attribute
	{
		Type complexParserType;

		public Type ComplexParser
		{
			get { return complexParserType; }
			set { complexParserType = value; }
		}
	}

	[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
	public class ParseMethodAttribute : Attribute
	{
	}

	[AttributeUsage(AttributeTargets.Class,AllowMultiple=false)]
	public class ParserAttribute : Attribute
	{
	}

	public interface IComplexParser
	{
		void Load(string str, XmlPositionMarker errorPos);
		CompilingReference[] GetReferences();
		object GetObject(object[] references);
	}

	/// <internal/>
	public class ReferenceParser<T> : IComplexParser
	{
		string name;
		XmlPositionMarker errorPos;

		public void Load(string name, XmlPositionMarker errorPos)
		{
			this.name = name;
			this.errorPos = errorPos;
		}

		public CompilingReference[] GetReferences()
		{
			return new CompilingReference[]
			{
				new CompilingReference(name,typeof(T),errorPos),
			};
		}

		public object GetObject(object[] references)
		{
			return references[0];
		}
	}

	public static class DidaXml
	{
		internal enum ParserType
		{
			/// <summary>
			/// the string will directly be parsed into a 
			/// value. This should be used for things like simple numeric values,
			/// geometric stuff, like vectors and rotations, and similar things.
			/// </summary>
			Value,

			/// <summary>
			/// The parsing is done using an <see cref = "IComplexParser"/>
			/// </summary>
			ComplexParser,

			/// <summary>
			/// The string represents a file name (that will be opened using the 
			/// <see cref = "IExternalsLoader"/>. 
			/// </summary>
			ExternalFile,
		}

		internal class Parser
		{
			/// <summary>
			/// Specifies how the given object should be parsed. See 
			/// <see cref = "ParserType"/> for a description of the different types.
			/// </summary>
			public ParserType parserType;

			/// <summary>
			/// Only used when <see cref = "parserType"/> is <see cref = "ParserType.Value"/>. 
			/// It specified the method that can parse a string into a value of 
			/// the corresponding type.
			/// </summary>
			/// <remarks>
			/// This method must be static, must take one string as argument (the string to parse),
			/// and must return an object of the type this parser is for.
			/// </remarks>
			public MethodInfo parserMethod;

			/// <summary>
			/// Only used when <see cref = "parserType"/> is <see cref = "ParserType.ComplexParser"/>
			/// It specifies an IComplexParser derived type that will be instanciated for
			/// each value of the corresponding type.
			/// </summary>
			public Type complexParserType;

			public Type targetType;
		}

		private static Dictionary<Type, Parser> parsers = new Dictionary<Type,Parser>();

		/// <summary>
		/// This dictionary maps xml tag names to the type of class that
		/// implements the IXmlObjectCompiler
		/// </summary>
		private static Dictionary<string,Type> objectCompilerTypes =
			new Dictionary<string,Type>();
		
		static DidaXml()
		{
			RegisterDefaultParsers();

			ScanTypeForParsable(typeof(TestType));
			ScanTypeForParsable(typeof(ReferenceTest));
			ScanTypeForDidaObject(typeof(TestType));
			ScanTypeForDidaObject(typeof(ReferenceTest));
		}

		private static void RegisterDefaultParsers()
		{
			Type[] parserArgs = new Type[] { typeof(string) };

			Parser floatParser = new Parser();
			floatParser.parserType = ParserType.Value;
			floatParser.parserMethod = typeof(DefaultParsers).GetMethod(
				"FloatParser", parserArgs);
			floatParser.targetType = typeof(float);
			parsers.Add(typeof(float), floatParser);

			Parser intParser = new Parser();
			intParser.parserType = ParserType.Value;
			intParser.parserMethod = typeof(int).GetMethod("Parse",parserArgs);
			intParser.targetType = typeof(int);
			parsers.Add(typeof(int), intParser);

			Parser stringParser = new Parser();
			stringParser.parserType = ParserType.Value;
			stringParser.parserMethod = typeof(DefaultParsers).GetMethod(
				"StringParser", parserArgs);
			stringParser.targetType = typeof(string);
			parsers.Add(typeof(string), stringParser);

			Parser boolParser = new Parser();
			boolParser.parserType = ParserType.Value;
			boolParser.parserMethod = typeof(DefaultParsers).GetMethod(
				"BoolParser", parserArgs);
			boolParser.targetType = typeof(bool);
			parsers.Add(typeof(bool), boolParser);

			Parser streamParser = new Parser();
			streamParser.parserType = ParserType.ExternalFile;
			streamParser.parserMethod = null;
			streamParser.targetType = typeof(Stream);
			parsers.Add(typeof(Stream), streamParser);
		}

		public static class DefaultParsers
		{
			public static float FloatParser(string s)
			{
				return float.Parse(s, CultureInfo.InvariantCulture);
			}

			public static string StringParser(string s)
			{
				return s;
			}

			public static bool BoolParser(string s)
			{
				if (s == "true")
					return true;
				else if (s == "false")
					return false;
				else
				{
					throw new Exception("A boolean value should be either \"true\" or \"false\".");
				}
			}
		}
		
		internal static Parser GetParser(Type type)
		{
			Debug.Assert(parsers.ContainsKey(type),
				"There is no parser for the type " + type.FullName);
			return parsers[type];
		}

		internal static Type GetObjectCompilerType(string tagName)
		{
			Type ret;
			if(objectCompilerTypes.TryGetValue(tagName,out ret))
				return ret;
			else
			{
				throw new Exception("Unknown tag name: \"" + tagName + "\".");
			}
		}

		public static void RegisterObjectCompiler(string tagName, Type compilerType)
		{
			Debug.Assert(!objectCompilerTypes.ContainsKey(tagName),
				"There's already a compiler for the given tag name.");
			Debug.Assert(compilerType.GetConstructor(Type.EmptyTypes) != null ||
				compilerType.GetConstructors().Length == 0,
				"The given type should have a constructor that takes 0 params, " + 
				"or should have no constructor at all");

			objectCompilerTypes.Add(tagName, compilerType);
		}

		public static void ScanAssembly(Assembly assembly)
		{
			foreach (Module module in assembly.GetModules())
				foreach (Type type in module.GetTypes())
					ScanTypeForParsable(type);

			foreach (Module module in assembly.GetModules())
				foreach (Type type in module.GetTypes())
					ScanTypeForDidaObject(type);
		}

		public static void ScanModule(Module module)
		{
			foreach (Type type in module.GetTypes())
				ScanTypeForParsable(type);

			foreach (Type type in module.GetTypes())
				ScanTypeForDidaObject(type);
		}

		public static void ScanTypeForParsable(Type type)
		{
			object[] attrs = type.GetCustomAttributes(false);
			foreach (object attr in attrs)
			{
				if (attr is ParsableAttribute)
				{
					ParsableAttribute parseableAttr = (ParsableAttribute)attr;
					if (type.IsEnum)
						RegisterParsableEnum(type, parseableAttr);
					else if (parseableAttr.ComplexParser != null)
						RegisterComplexParser(type, parseableAttr);
					else
						RegisterParsableObject(type, parseableAttr);
				}

				if (attr is Dxml.DidaObjectAttribute)
				{
					// it's a dida object, so we are going to register a
					// reference parser

					// the parser is a reference parser
					Parser parser = new Parser();
					parser.parserType = ParserType.ComplexParser;
					parser.complexParserType = typeof(ReferenceParser<>).MakeGenericType(type);
					parser.targetType = type;
					parsers.Add(type, parser);
				}
			}
		}

		public static void ScanTypeForDidaObject(Type type)
		{
			object[] attrs = type.GetCustomAttributes(false);
			foreach (object attr in attrs)
			{
				if (attr is DidaObjectAttribute)
				{
					RegisterDidaObject(type, (DidaObjectAttribute)attr);
				}
			}
		}

		private static void RegisterDidaObject(Type type, DidaObjectAttribute attr)
		{
			string tagName;
			if (attr.TagName != null)
				tagName = attr.TagName;
			else
				tagName = type.Name;

			// look for an xml constructor
			ConstructorInfo[] ctors = type.GetConstructors();
			foreach (ConstructorInfo ctor in ctors)
			{
				object[] ctorAttrs = ctor.GetCustomAttributes(
					typeof(XmlConstructorAttrBase), false);
				Debug.Assert(ctorAttrs.Length < 2);
				if (ctorAttrs.Length == 1)
				{
					XmlConstructorAttrBase ctorAttr = (XmlConstructorAttrBase)ctorAttrs[0];

					Debug.Assert(!objectCompilerTypes.ContainsKey(tagName),
						"There's already a loader for the tagname \"" + tagName + "\"");
					
					objectCompilerTypes[tagName] = ctorAttr.CreateCompiler(tagName, type, ctor); 					
				}
			}

			if (attr.Compiler != null)
			{
				Debug.Assert(!objectCompilerTypes.ContainsKey(tagName),
					"There's already a loader for the tagname \"" + tagName + "\"");
				objectCompilerTypes[tagName] = attr.Compiler;
			}
		}

		private static void RegisterParsableObject(Type type,
			ParsableAttribute attr)
		{
			Parser parser = new Parser();
			parser.parserType = ParserType.Value;
			parser.targetType = type;

			// find the parser method
			parser.parserMethod = null;
			foreach (MethodInfo method in type.GetMethods())
			{
				object[] attrs = method.GetCustomAttributes(typeof(ParseMethodAttribute),false);
				Debug.Assert(attrs.Length < 2, "A method shouldn't have more than 1 DidaParseMethod attribute");
				
				if (attrs.Length != 0)
				{
					// it does have a DidaParseMethod attribute

					Debug.Assert(parser.parserMethod == null, "A type should only have one parse method (A method marked with the DidaParseMethod attribute)");
					Debug.Assert(method.IsStatic, "A parse method should be static");
					Debug.Assert(method.GetParameters().Length == 1, "A parse method should take 1 argument, of type string");
					Debug.Assert(method.GetParameters()[0].ParameterType == typeof(string), "A parse method should take 1 argument, of type string");

					parser.parserMethod = method;
#if !DEBUG
					// if we're in debug mode, we look for other methods, to make sure
					// this is the only parse method.
					break;
#endif
				}
			}

			Debug.Assert(parser.parserMethod != null, "The type " + type.FullName + "doesn't have a DidaParsable marked method");
			Debug.Assert(parser.parserMethod.ReturnType == type, "The parser method should return a variable of the same type it was registered for");

			parsers.Add(type,parser);
		}

		private static void RegisterComplexParser(Type type, ParsableAttribute attr)
		{
			Debug.Fail("Didn't know this was used");

			Parser parser = new Parser();
			parser.parserType = ParserType.ComplexParser;
			parser.complexParserType = attr.ComplexParser;
			parser.targetType = type;
			parsers.Add(type,parser);
		}

		private static void RegisterParsableEnum(Type type, ParsableAttribute attr)
		{
			Parser parser = new Parser();
			parser.parserType = ParserType.Value;
			parser.targetType = type;

			SortedList<string, int> nameToValue = new SortedList<string, int>();
			foreach (FieldInfo field in type.GetFields())
			{
				Debug.Assert(field.IsStatic || field.IsSpecialName, "An enum should only contain static fields");
				if (!field.IsSpecialName)
				{
					nameToValue.Add(field.Name, (int)field.GetValue(null));
				}
			}

			parser.parserMethod = CodeGenToolkit.EmitBinarySearch(
				nameToValue, type);
			parsers.Add(type,parser);
			
		}

		public static void RegisterComplexParser(Type targetType,Type parserType)
		{
			Debug.Assert(!parsers.ContainsKey(targetType),
				"There is already a parser for the given type");

			Parser parser = new Parser();
			parser.parserType = ParserType.ComplexParser;
			parser.complexParserType = parserType;
			parser.targetType = targetType;
			parsers.Add(targetType, parser);
		}

		public static void Finish()
		{
			// TODO: Make a better way for doing this.
			SerializationAssembly.Finish();
		}

		public static XmlSchema XmlSchema
		{
			get
			{
				XmlSchema schema = new XmlSchema();
				schema.TargetNamespace = "http://www.quasar3d.nl/Dida";

				XmlSchemaAttribute nameAttrib = new XmlSchemaAttribute();
				nameAttrib.Name = "ObjName";
				nameAttrib.SchemaType = XmlSchemaSimpleType.GetBuiltInSimpleType(XmlTypeCode.String);
				schema.Items.Add(nameAttrib);

				foreach (KeyValuePair<string,Type> cur in objectCompilerTypes)
				{
					XmlSchemaElement element = new XmlSchemaElement();
					element.Name = cur.Key;
					schema.Items.Add(element);

					XmlSchemaComplexType typeDesc = new XmlSchemaComplexType();
					element.SchemaType = typeDesc;
					
					XmlSchemaAttribute nameAttr = new XmlSchemaAttribute();
					nameAttr.Name = "name";
					nameAttr.Use = XmlSchemaUse.Required;
					nameAttr.SchemaTypeName = new XmlQualifiedName("ObjName");
					typeDesc.Attributes.Add(nameAttr);

					XmlSchemaAttribute exportAttr = new XmlSchemaAttribute();
					exportAttr.Name = "export";
					exportAttr.DefaultValue = "false";
					exportAttr.Use = XmlSchemaUse.Optional;
					exportAttr.SchemaTypeName = new XmlQualifiedName("boolean", XmlSchema.Namespace);
					typeDesc.Attributes.Add(exportAttr);

					//XmlSchemaComplexContent content = new XmlSchemaComplexContent();
					//element.Constraints.Add(content);
				}

				return schema;
			}
		}
	}
}